#!/usr/bin/env python

"""
Library that provides a set of classes that work with the Pownce API
"""

__author__  = 'bear@code-bear.com'
__version__ = '0.1'
__license__ = """
Copyright (c) 2007 Mike Taylor
All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import os, sys
import time
import random
import httplib2
import urllib
import hashlib, base64, binascii
import xml.etree.ElementTree
import mimetypes
import email.encoders
import email.mime.base
import feedparser


MIME_BOUNDARY = '--------------bears_magical_boundary_value_$'


class Session(object):
    """
    A single Pownce session.
    
    Each instance of the Session class represents a single Pownce session and
    maintains all of the necessary state information from the inital login,
    any posts or reads, and on to the final logout.
    
    If specified, it will use a cache directory to store any intermediate files
    that are seen during the current session.
    """
    def __init__(self, cachedir=None):
        self.userid    = None
        self.password  = None
        self.apitoken  = None
        self.sessionid = None
        self.userinfo  = {}
        self.sets      = {}
        self.friends   = {}
        self.fan_of    = {}
        self.userAgent = 'PyPownce/%s +http://code-bear/code/pypownce/' % __version__

        if cachedir is None:
            self.cachedir = os.path.abspath(os.path.expanduser('~/.cache'))
        else:
            self.cachedir = cachedir

        self.http = httplib2.Http(self.cachedir)

        self.http.follow_redirects = True

    def _gen_wsse_auth(self):
        nonce  = hashlib.md5('%s:%s' % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).hexdigest()[:16]
        c_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
        passwd = base64.b64encode(hashlib.sha1('%s%s%s' % (nonce, c_time, self.apitoken)).digest())

        return 'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"' % (self.userid, passwd, nonce, c_time)

    def _fetch(self, url, headers=None, post=None, multipart=False):
        hdrs = {}

        if headers is not None:
            for key in headers:
                hdrs[key] = headers[key]

        if 'UserAgent' not in hdrs:
            hdrs['User-Agent'] = self.userAgent

        if self.sessionid is not None:
            hdrs['Cookie'] = 'sessionid=%s' % self.sessionid

        if post is not None:
            method = 'POST'
            auth   = self._gen_wsse_auth()

            if multipart:
                body  = '%s\nContent-Disposition: form-data; name="auth"\n\n%s\n' % (MIME_BOUNDARY, auth)
                body += post

                hdrs['Content-Type']   = 'multipart/form-data; boundary=%s' % MIME_BOUNDARY
                hdrs['Content-Length'] = str(len(body))

                print body
            else:
                body = 'auth=%s&%s' % (auth, post)
        else:
            method = 'GET'
            body   = None

        try:
            resp, content = self.http.request(url, method=method, body=body, headers=hdrs)

        except httplib2.HttpLib2Error, e:
            if hasattr(e, 'response'):
                resp    = e.response
                content = e.content
            else:
                resp    = {'status': '1'}
                content = e.desc

        return resp, content

    def login(self, userid, password):
        self.userid   = userid
        self.password = password

        self.http.clear_credentials()
        self.http.add_credentials(userid, password)

        # not sure if this is *really* needed, but for now...
        resp, content = self._fetch('http://pownce.com/api/available')

        # do a basic auth GET - the body contains user info
        resp, content = self._fetch('http://pownce.com/api/login')

        if 'set-cookie' in resp:
            try:
                self.sessionid = resp['set-cookie'].split(';')[0].split('=')[1]
            except:
                self.sessionid = None

        dom = xml.etree.ElementTree.fromstring(content)

        for element in dom.getiterator(): #'{http://pownce.com/Atom}login'):
            if element.tag == '{http://pownce.com/Atom}login':
                self.apitoken = element.attrib['token']
            elif element.tag == '{http://pownce.com/Atom}user':
                self.userinfo['pro'] = element.attrib['pro'] == '1'
            elif element.tag == '{http://pownce.com/Atom}name':
                self.userinfo['name'] = element.text
            elif element.tag == '{http://pownce.com/Atom}uri':
                self.userinfo['uri'] = element.text
            elif element.tag == '{http://pownce.com/Atom}image':
                self.userinfo['image'] = element.text

        if self.apitoken is not None:
            self.http.add_credentials(userid, self.apitoken)

        self.getFriends()
        self.getFanOf()

    def getFriends(self):
        """
        Retrieve the api/recipients/ page, parse the HTML and build the list of sets and friends
        """
        response, content = self._fetch('http://pownce.com/api/recipients/', post='')

        if response['status'] == '200':
            self.sets    = { 'public': 'the public', }
            self.friends = {}

            dom = xml.etree.ElementTree.fromstring(content)

            for element in dom.getiterator():
                if element.tag[-6:] == 'option':
                    s = element.attrib['value']
                    if s.startswith('set'):
                        self.sets[s] = element.text
                    elif s.startswith('friend'):
                        self.friends[s] = element.text

    def getFanOf(self):
        """
        Retrieve the api/recipients/ page, parse the HTML and build the list of sets and friends
        """
        response, content = self._fetch('http://pownce.com/bear/fan_of/', post='')

        if response['status'] == '200':
            self.fan_of = {}

            # add parsing of html returned

class Feed(object):
    """
    Helper class to manage the sending (push) and retrieving (pull)
    of all Pownce items for the user.
    
    Requires an active pownce Session.
    
    All of the heavy lifting for parsing the Atom feed is handled by feedparser.
    """
    def __init__(self, session=None):
        self.session  = session
        self.items    = {}
        self.feed     = None
        self.lastpoll = None
        self.error    = ''

    def _parsefeed(self):
        """
        Walk the Atom feed data returned by feedparser, create an entry for each
        item and store that in the feed attribute.
        
        Currently no image or secondary link retrieval is done - but the
        information is stored.
        
        Also, author information is duplicated and not yet managed via the
        session's friend list.
        """
        # {'username':       u'ryanstewart', 
        #  'updated':        u'2007-07-10T01:29:22Z', 
        #  'links':          [{'href': u'http://pownce.com/api/notes/127156/', 
        #                      'type': u'application/atom+xml', 
        #                      'rel': u'self'},
        #                     {'href': u'http://pownce.com/ryanstewart/notes/127156/', 
        #                      'type': u'text/html', 
        #                      'rel': u'alternate'}],
        #  'recipients':     u'', 
        #  'title':          u'ryanstewart: Breaking open and configuring a brand new Mac Book Pro....', 
        #  'image':          {},
        #  'author':         u'Ryan S.', 
        #  'summary_detail': {'base':     '', 
        #                     'type':     'text/plain', 
        #                     'value':    u'Breaking open and configuring a brand new Mac Book Pro. I love the feeling of a new computre. Except in this case my computer is missing half of its memory because it came in two shipments.', 
        #                     'language': None},
        #  'summary':        u'Breaking open and configuring a brand new Mac Book Pro. I love the feeling of a new computre. Except in this case my computer is missing half of its memory because it came in two shipments.', 
        #  'title_detail':   {'base':     '', 
        #                     'type':     'text/plain', 
        #                     'value':    u'ryanstewart: Breaking open and configuring a brand new Mac Book Pro....', 
        #                     'language': None}, 
        #  'href':           u'http://pownce.com/ryanstewart/', 
        #  'link':           u'http://pownce.com/ryanstewart/notes/127156/', 
        #  'stars':          u'0.0', 
        #  'replies':        u'1', 
        #  'author_detail': {'href': u'http://pownce.com/ryanstewart/', 
        #                    'name': u'Ryan S.'}, 
        #  'id':            u'urn:uuid:ryanstewart:127156', 
        #  'updated_parsed': (2007, 7, 10, 1, 29, 22, 1, 191, 0)}, 

        for entry in self.feed.entries:
            itemid = entry['id']

            if itemid in self.items:
                item = self.items[itemid]

                for key in ['updated', 'stars', 'replies', 'recipients']:
                    item[key] = entry[key]
            else:
                item = {}

                for key in ['username', 'author', 'updated', 'recipients', 'title', 'summary', 'link', 'stars', 'replies']:
                    item[key] = entry[key]

                self.items[itemid] = item

    def push(self, item):
        """
        Push a Feed item to Pownce.
        
        All detail information is read from the passed in item.
        
        The resulting Atom entry is parsed and added to the feed
        list.
        """
        if item is not None:
            items   = {}
            headers = { 'Content-Type': 'application/x-www-form-urlencoded', }

            response, content = self.session._fetch('http://pownce.com/api/notes/', post=item.data(), headers=headers, multipart=item.multipart)

            print content

            entries = feedparser.parse(content)

            print entries

            for entry in entries:
                itemid = entry['id']

                if itemid in items:
                    item = items[itemid]

                    for key in ['updated', 'stars', 'replies', 'recipients']:
                        item[key] = entry[key]
                else:
                    item = {}

                    for key in ['username', 'author', 'updated', 'recipients', 'title', 'summary', 'link', 'stars', 'replies']:
                        item[key] = entry[key]

                    items[itemid] = item

                print item

    def pull(self):
        """
        Pull the user's notes, parse and store in the feed item list.
        """
        result = False

        self.data     = None
        self.response = None

        if self.session is None:
            self.error = 'items pull requires an active Session'
        else:
            now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

            if now == self.lastpoll:
                self.error = 'polled within a minute of last poll'
            else:
                self.response, content = self.session._fetch('http://pownce.com/api/notes/for/%s/' % self.session.userid, post='')

                if self.response['status'] == '200':
                    self.feed = feedparser.parse(content)

                    self._parsefeed()

                    result = True
                else:
                    self.error = content

            self.lastpoll = now

        return result


class Items(object):
    """
    Base class for all postable items.
    
    Message, Event, Link and File
    """
    def __init__(self):
        self.type      = None
        self.body      = None
        self.to        = None
        self.multipart = False   # flags the Feed.push() routine to handle as mime-multipart

    def data(self):
        """
        Abstract method - must be overriden
        """
        raise


class Message(Items):
    """
    Represents a single Pownce Message type.
    """
    def __init__(self, text='', to=None):
        self.type = 'note-message'
        self.body = text
        self.to   = to

    def data(self):
        """
        Format the post data string for a Message type
        """
        return 'note%%5Ftype=%s&note%%5Fbody=%s&note%%5Fto=%s' % (self.type, urllib.quote(self.body), self.to)


class Event(Items):
    """
    Represents a single Pownce Event type.
    """
    def __init__(self, text='', to=None, location='', name='', datetime=None):
        self.type     = 'note-event'
        self.body     = text
        self.to       = to
        self.name     = name
        self.location = location

        if datetime is None:
            self.datetime = time.localtime()
        else:
            self.datetime = datetime

    def data(self):
        """
        Format the post data string for an Event type
        """
        t  = time.strftime('%I:%M %p', self.datetime)

        s  = 'note%%5Ftype=%s&note%%5Fbody=%s&note%%5Fto=%s' % (self.type, urllib.quote(self.body), self.to)
        s += 'event%%5Fname=%s&&event%%5Flocation=%s&event%%5Ftime=%s&event%%5Fdate%5Fyear=%s&event%%5Fdate%5Fmonth=%s&event%%5Fdate%5Fday=%s' % \
             (self.name, self.location, t, self.datetime[0], self.datetime[1], self.datetime[2])

        return s


class Link(Items):
    """
    Represents a single Pownce Link type.
    """
    def __init__(self, text='', to=None, url=''):
        self.type = 'note-link'
        self.body = text
        self.to   = to
        self.url  = url

    def data(self):
        """
        Format the post data string for an Link type
        """
        s = 'note%%5Ftype=%s&note%%5Fbody=%s&note%%5Fto=%s&url=%s' % (self.type, urllib.quote(self.body), self.to, self.url)

        return s


class File(Items):
    """
    Represents a single Pownce File type.
    """
    def __init__(self, text='', to=None, filename=''):
        self.type      = 'note-file'
        self.body      = text
        self.to        = to
        self.filename  = filename
        self.multipart = True
        self.mimetype  = (None, None)

    def data(self):
        """
        Format the post data string for an File type
        """
        if os.path.exists(self.filename):
            filename = os.path.basename(self.filename)
            ctype    = self.mimetype[0]

            if ctype is None:
                ctype = mimetypes.guess_type(self.filename)[0]

                if ctype is None:
                    ctype = 'application/octet-stream'

            mtype, stype = ctype.split('/', 1)

            msg = email.mime.base.MIMEBase(mtype, stype)
            msg.set_payload(open(self.filename, 'rb').read())
            email.encoders.encode_base64(msg)

            s = '%s\n' % MIME_BOUNDARY
            for key, value in [('note_type', self.type), ('note_body', self.body),
                               ('note_to',   self.to), ('Upload', 'Submit Query'),]:
                s += 'Content-Disposition: form-data; name="%s"\n\n%s\n%s\n' % (key, value, MIME_BOUNDARY)

            s += 'Content-Disposition: form-data; name="media_file"; filename="%s"\n\n%s\n%s\n' % (filename, msg.get_payload(), MIME_BOUNDARY)
        else:
            s = None

        return s


